<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Proxy</title>
</head>
<body>
<script type="text/javascript">
    {
        /**
         * Proxy 用于修改某些操作的默认行为，等同于在语言层面做出修改，所以属于一种“元编程”(meta programming)，即对编程语言进行编程
         * 可以理解成，在目标对象之前架设一层“拦截”，外界对该对象的访问，都必须先通过这层拦截，因此提供了一种机制，可以对外界的访问进行过滤和改写。
         * Proxy 这个词的原意是代理，用在这里表示由它来“代理”某些操作，可以译为“代理器”。
         */
        const obj = new Proxy({}, {
            get: function (target, key, receiver) {
                console.log(`getting ${key}!`);
                return Reflect.get(target, key, receiver);
            },
            set: function (target, key, value, receiver) {
                console.log(`setting ${key}!`);
                return Reflect.set(target, key, value, receiver);
            }
        });

        console.log(obj.count = 1); // setting count! 1
        console.log(++obj.count); // getting count! setting count! 2

        const proxy = new Proxy({}, {
            get: function (target, property) {
                return 35;
            }
        });
        /*上面代码中，作为构造函数，Proxy接受两个参数。
        第一个参数是所要代理的目标对象(上例是一个空对象)，即如果没有Proxy的介入，操作原来要访问的就是这个对象；
        第二个参数是一个配置对象，对于每一个被代理的操作，需要提供一个对应的处理函数，该函数将拦截对应的操作。
        比如，上面代码中，配置对象有一个get方法，用来拦截对目标对象属性的访问请求。
        get方法的两个参数分别是目标对象和所要访问的属性。
        可以看到，由于拦截函数总是返回35，所以访问任何属性都得到35。*/
        console.log(proxy.name); // 35
        console.log(proxy.time); // 35
        console.log(proxy.title); // 35

        /*如果handler没有设置任何拦截，那就等同于直接通向原对象*/
        const target = {};
        const handler = {};
        const proxy1 = new Proxy(target, handler);
        proxy1.a = 'b';
        /*上面代码中，handler是一个空对象，没有任何拦截效果，访问handler就等同于访问target*/
        console.log(target.a); // b

        /*Proxy 实例也可以作为其他对象的原型对象。*/
        const proxy2 = new Proxy({}, {
            get: function (target, property) {
                return 35;
            }
        });

        let object = Object.create(proxy2);
        /*上面代码中，proxy对象是obj对象的原型，obj对象本身并没有time属性，所以根据原型链，会在proxy对象上读取该属性，导致被拦截。*/
        console.log(object); // {}
        console.log(object.time); // 35

        /*同一个拦截器函数，可以设置拦截多个操作*/
        const hand = {
            get: function (target, name) {
                if (name === 'prototype') {
                    return Object.prototype;
                }
                return 'Hello, ' + name;
            },

            apply: function (target, thisBinding, args) {
                return args[0];
            },

            construct: function (target, args) {
                return {value: args[1]};
            }
        };
        let proxy3 = new Proxy(function (x, y) {
            return x + y;
        }, hand);
        console.log(proxy3(1, 2)); // 3
        console.log(new proxy3(1, 2)); // {value: 2}
        console.log(proxy3.prototype === Object.prototype); // true
        console.log(proxy3.foo); // Hello, foo

        /*Proxy 支持的拦截操作一览*/
        /*对于可以设置，但没有设置拦截的操作，则直接落在目标对象上，按照原先的方式产生结果*/
        /*1、get(target, propKey, receiver)：拦截对象属性的读取，比如proxy.foo和proxy['foo']，最后一个参数receiver是一个对象，可选，参见下面Reflect.get的部分*/
        /*2、set(target, propKey, value, receiver)：拦截对象属性的设置，比如proxy.foo = v或proxy['foo'] = v，返回一个布尔值*/
        /*3、has(target, propKey)：拦截propKey in proxy的操作，以及对象的hasOwnProperty方法，返回一个布尔值*/
        /*4、deleteProperty(target, propKey)：拦截delete proxy[propKey]的操作，返回一个布尔值*/
        /*5、ownKeys(target)：拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)，返回一个数组。该方法返回对象所有自身的属性，而Object.keys()仅返回对象可遍历的属性*/
        /*6、getOwnPropertyDescriptor(target, propKey)：拦截Object.getOwnPropertyDescriptor(proxy, propKey)，返回属性的描述对象*/
        /*7、defineProperty(target, propKey, propDesc)：拦截Object.defineProperty(proxy, propKey, propDesc）、Object.defineProperties(proxy, propDescs)，返回一个布尔值*/
        /*8、preventExtensions(target)：拦截Object.preventExtensions(proxy)，返回一个布尔值*/
        /*9、getPrototypeOf(target)：拦截Object.getPrototypeOf(proxy)，返回一个对象*/
        /*10、isExtensible(target)：拦截Object.isExtensible(proxy)，返回一个布尔值*/
        /*11、setPrototypeOf(target, proto)：拦截Object.setPrototypeOf(proxy, proto)，返回一个布尔值*/
        /*如果目标对象是函数，那么还有两种额外操作可以拦截*/
        /*12、apply(target, object, args)：拦截 Proxy 实例作为函数调用的操作，比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)*/
        /*13、construct(target, args)：拦截 Proxy 实例作为构造函数调用的操作，比如new proxy(...args)*/
    }
    {
        /**
         * Proxy实例的方法
         * 1、get()：用于拦截某个属性的读取操作，get方法可以继承
         * 2、set()：用来拦截某个属性的赋值操作
         * 3、apply()：拦截函数的调用、call和apply操作
         * 4、has()：拦截HasProperty操作，即判断对象是否具有某个属性时，这个方法会生效。典型的操作就是in运算符
         * 5、construct()：用于拦截new命令，下面是拦截对象的写法
         * 6、deleteProperty()：用于拦截delete操作，如果这个方法抛出错误或者返回false，当前属性就无法被delete命令删除
         * 7、defineProperty()：拦截了Object.defineProperty操作
         * 8、getOwnPropertyDescriptor()：拦截Object.getOwnPropertyDescriptor，返回一个属性描述对象或者undefined
         * 9、getPrototypeOf()：主要用来拦截Object.getPrototypeOf()运算符，以及其他一些操作
         * 10、isExtensible()：拦截Object.isExtensible操作
         * 11、ownKeys()：拦截Object.keys()操作
         * 12、preventExtensions()：拦截Object.preventExtensions()。该方法必须返回一个布尔值
         */
        /*1、get()*/
        const person = {
            name: "张三"
        };

        let proxy = new Proxy(person, {
            get: function (target, property) {
                if (property in target) {
                    return target[property];
                } else {
                    throw new ReferenceError("Property \"" + property + "\" does not exist.");
                }
            }
        });
        console.log(proxy.name); // 张三
        // console.log(proxy.age); // Property "age" does not exist.

        let proto = new Proxy({}, {
            get(target, propertyKey, receiver) {
                console.log('GET ' + propertyKey);
                return target[propertyKey];
            }
        });
        let obj = Object.create(proto);
        console.log(obj.xxx); // GET xxx

        /*下面的例子使用get拦截，实现数组读取负数的索引*/
        function createArray(...elements) {
            let handler = {
                get(target, propKey, receiver) {
                    let index = Number(propKey);
                    if (index < 0) {
                        propKey = String(target.length + index);
                    }
                    return Reflect.get(target, propKey, receiver);
                }
            };

            let target = [];
            target.push(...elements);
            return new Proxy(target, handler);
        }

        let arr = createArray('a', 'b', 'c');
        console.log(arr[-1]); // c

        /*利用 Proxy，可以将读取属性的操作(get)，转变为执行某个函数，从而实现属性的链式操作。*/
        let pipe = (function () {
            return function (value) {
                let funcStack = [];
                let oproxy = new Proxy({}, {
                    get: function (pipeObject, fnName) {
                        if (fnName === 'get') {
                            return funcStack.reduce(function (val, fn) {
                                // return fn(val);
                            }, value);
                        }
                        funcStack.push(window[fnName]);
                        return oproxy;
                    }
                });

                return oproxy;
            }
        }());
        let double = n => n * 2;
        let pow = n => n * n;
        let reverseInt = n => n.toString().split("").reverse().join("") | 0;
        console.log(pipe(3).double.pow.reverseInt.get); // 63

        /*下面的例子则是利用get拦截，实现一个生成各种DOM节点的通用函数dom*/
        const dom = new Proxy({}, {
            get(target, property) {
                return function (attrs = {}, ...children) {
                    const el = document.createElement(property);
                    for (let prop of Object.keys(attrs)) {
                        el.setAttribute(prop, attrs[prop]);
                    }
                    for (let child of children) {
                        if (typeof child === 'string') {
                            child = document.createTextNode(child);
                        }
                        el.appendChild(child);
                    }
                    return el;
                }
            }
        });
        const el = dom.div({},
            'Hello, my name is ',
            dom.a({href: '//example.com'}, 'Mark'),
            '. I like:',
            dom.ul({},
                dom.li({}, 'The web'),
                dom.li({}, 'Food'),
                dom.li({}, '…actually that\'s it')
            )
        );
        console.log(document.body.appendChild(el));
    }
    {
        /*2、set()*/
        /*假定Person对象有一个age属性，该属性应该是一个不大于200的整数，那么可以使用Proxy保证age的属性值符合要求。*/
        let validator = {
            set: function (obj, prop, value) {
                if (prop === 'age') {
                    if (!Number.isInteger(value)) {
                        throw new TypeError('The age is not an integer');
                    }
                    if (value > 200) {
                        throw new RangeError('The age seems invalid');
                    }
                }

                // 对于age以外的属性，直接保存
                obj[prop] = value;
            }
        };

        let person = new Proxy({}, validator);
        person.age = 100;
        console.log(person.age); // 100
        // person.age = 'young';
        // console.log(person.age); // The age is not an integer
        // person.age = 300;
        // console.log(person.age); // The age seems invalid

        /*有时，我们会在对象上面设置内部属性，属性名的第一个字符使用下划线开头，表示这些属性不应该被外部使用。结合get和set方法，就可以做到防止这些内部属性被外部读写。*/
        let handler = {
            get(target, key) {
                invariant(key, 'get');
                return target[key];
            },
            set(target, key, value) {
                invariant(key, 'set');
                target[key] = value;
                return true;
            }
        };

        function invariant(key, action) {
            if (key[0] === '_') {
                throw new Error(`Invalid attempt to ${action} private "${key}" property`);
            }
        }

        /*只要读写的属性名的第一个字符是下划线，一律抛错，从而达到禁止读写内部属性的目的*/
        const target = {};
        const proxy = new Proxy(target, handler);
        // console.log(proxy._prop); // Invalid attempt to get private "_prop" property
        // console.log(proxy._prop = 'c'); // Invalid attempt to get private "_prop" property
    }
    {
        /*3、apply()：apply方法可以接受三个参数，分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组*/
        /*const handler = {
            apply(target, ctx, args) {
                return Reflect.apply(...arguments);
            }
        };*/

        /*例子*/
        const target = function () {
            return 'I am the target';
        };
        const handler = {
            apply: function () {
                return 'I am the proxy';
            }
        };
        let p = new Proxy(target, handler);
        console.log(p()); // I am the proxy

        /*另外一个例子*/
        const twice = {
            apply(target, ctx, args) {
                return Reflect.apply(...arguments) * 2;
            }
        };

        function sum(left, right) {
            return left + right;
        }

        const proxy = new Proxy(sum, twice);
        /*每当执行proxy函数(直接调用或call和apply调用)，就会被apply方法拦截。另外，直接调用Reflect.apply方法，也会被拦截*/
        console.log(proxy(1, 2)); // 6
        console.log(proxy.call(null, 5, 6)); // 22
        console.log(proxy.apply(null, [7, 8])); // 30
        console.log(Reflect.apply(proxy, null, [9, 10])); // 38
    }
    {
        /*4、has()：拦截HasProperty操作，即判断对象是否具有某个属性时，这个方法会生效。典型的操作就是in运算符*/
        const handler = {
            has(target, key) {
                if (key[0] === '_') {
                    return false;
                }
                return key in target;
            }
        };
        const target = {_prop: 'foo', prop: 'foo'};
        let proxy = new Proxy(target, handler);
        console.log('_prop' in proxy); // false
        console.log('prop' in proxy); // true

        /*如果原对象不可配置或者禁止扩展，这时has拦截会报错*/
        const obj = {a: 10};
        Object.preventExtensions(obj);
        const p = new Proxy(obj, {
            has: function (target, prop) {
                return false;
            }
        });
        // console.log('a' in p); // 'has' on proxy: trap returned falsish for property 'a' but the proxy target is not extensible

        /*值得注意的是，has方法拦截的是HasProperty操作，而不是HasOwnProperty操作，即has方法不判断一个属性是对象自身的属性，还是继承的属性*/
        /*另外，虽然for...in循环也用到了in运算符，但是has拦截对for...in循环不生效*/
        let stu1 = {name: '张三', score: 59};
        let stu2 = {name: '李四', score: 99};
        let stuHandler = {
            has(target, prop) {
                if (prop === 'score' && target[prop] < 60) {
                    console.log(`${target.name} 不及格`);
                    return false;
                }
                return prop in target;
            }
        };
        let oproxy1 = new Proxy(stu1, stuHandler);
        let oproxy2 = new Proxy(stu2, stuHandler);
        console.log('score' in oproxy1); // 张三 不及格 false
        console.log('score' in oproxy2); // true
        for (let a in oproxy1) {
            console.log(oproxy1[a]); // 张三 59
        }
        for (let b in oproxy2) {
            console.log(oproxy2[b]); // 李四 99
        }
        /*上面代码中，has拦截只对in循环生效，对for...in循环不生效，导致不符合要求的属性没有被排除在for...in循环之外*/
    }
    {
        /*5、construct()：用于拦截new命令，下面是拦截对象的写法。接受两个参数：target: 目标对象；args：构建函数的参数对象*/
        /*const handler = {
            construct(target, args, newTarget) {
                return new target(...args);
            }
        };*/
        const p = new Proxy(function () {
        }, {
            construct: function (target, args) {
                console.log('called: ' + args.join(', '));
                return {value: args[0] * 10};
            }
        });
        console.log(new p(1).value); // called: 1 10

        /*construct方法返回的必须是一个对象，否则会报错*/
        const person = new Proxy(function () {
        }, {
            construct: function (target, argumentsList) {
                return 1;
            }
        });
        // console.log(new person()); // construct' on proxy: trap returned non-object ('1')
    }
    {
        /*6、deleteProperty()：用于拦截delete操作，如果这个方法抛出错误或者返回false，当前属性就无法被delete命令删除*/
        const handler = {
            deleteProperty(target, key) {
                invariant(key, 'delete');
                return true;
            }
        };

        function invariant(key, action) {
            if (key[0] === '_') {
                throw new Error(`Invalid attempt to ${action} private "${key}" property`);
            }
        }

        const target = {_prop: 'foo'};
        const proxy = new Proxy(target, handler);
        // console.log(delete proxy._prop); // Invalid attempt to delete private "_prop" property
        /*上面代码中，deleteProperty方法拦截了delete操作符，删除第一个字符为下划线的属性会报错*/
    }
    {
        /*7、defineProperty()：拦截了Object.defineProperty操作*/
        const handler = {
            defineProperty(target, key, descriptor) {
                return false;
            }
        };
        let target = {};
        let proxy = new Proxy(target, handler);
        console.log(proxy.foo = 'bar'); // bar
    }
    {
        /*8、getOwnPropertyDescriptor()：拦截Object.getOwnPropertyDescriptor，返回一个属性描述对象或者undefined*/
        let handler = {
            getOwnPropertyDescriptor(target, key) {
                if (key[0] === '_') {
                    return;
                }
                return Object.getOwnPropertyDescriptor(target, key);
            }
        };
        const target = {_foo: 'bar', baz: 'tar'};
        let proxy = new Proxy(target, handler);
        console.log(Object.getOwnPropertyDescriptor(proxy, 'war')); // undefined
        console.log(Object.getOwnPropertyDescriptor(proxy, '_foo')); // undefined，对于第一个字符为下划线的属性名会返回undefined
        console.log(Object.getOwnPropertyDescriptor(proxy, 'baz')); // {value: "tar", writable: true, enumerable: true, configurable: true}
    }
    {
        /*9、getPrototypeOf()：主要用来拦截Object.getPrototypeOf()运算符，以及其他一些操作*/
        /*Object.prototype.__proto__
        * Object.prototype.isPrototypeOf()
        * Object.getPrototypeOf()
        * Reflect.getPrototypeOf()
        * instanceof运算符*/
        let proto = {};
        let p = new Proxy({}, {
            getPrototypeOf(target) {
                return proto;
            }
        });
        console.log(Object.getPrototypeOf(p) === proto); // true
    }
    {
        /*10、isExtensible()：拦截Object.isExtensible操作*/
        let proxy = new Proxy({}, {
            isExtensible: function (target) {
                console.log("called");
                return true;
            }
        });
        console.log(Object.isExtensible(proxy)); // called true

        /*这个方法有一个强限制，如果不能满足下面的条件，就会抛出错误*/
        let p = new Proxy({}, {
            isExtensible: function (target) {
                return false;
            }
        });
        // console.log(Object.isExtensible(p)); // TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
    }
    {
        /*11、ownKeys()：拦截Object.keys()操作*/
        let target = {};

        let handler = {
            ownKeys(target) {
                return ['hello', 'world'];
            }
        };

        let proxy = new Proxy(target, handler);
        console.log(Object.keys(proxy)); // [ 'hello', 'world' ]

        /*下面的例子是拦截第一个字符为下划线的属性名*/
        let target2 = {
            _bar: 'foo',
            _prop: 'bar',
            prop: 'baz'
        };

        let handler2 = {
            ownKeys(target) {
                return Reflect.ownKeys(target).filter(key => key[0] !== '_');
            }
        };
        let proxy2 = new Proxy(target2, handler2);
        for (let key of Object.keys(proxy2)) {
            console.log(target2[key]); // baz
        }
    }
    {
        /*12、preventExtensions()：拦截Object.preventExtensions()。该方法必须返回一个布尔值*/
        /*这个方法有一个限制，只有当Object.isExtensible(proxy)为false(即不可扩展)时，proxy.preventExtensions才能返回true，否则会报错*/
        let p = new Proxy({}, {
            preventExtensions: function (target) {
                return true;
            }
        });
        // console.log(Object.preventExtensions(p)); // Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
        /*上面代码中，proxy.preventExtensions方法返回true，但这时Object.isExtensible(proxy)会返回true，因此报错*/
        /*为了防止出现这个问题，通常要在proxy.preventExtensions方法里面，调用一次Object.preventExtensions*/
        let proxy = new Proxy({}, {
            preventExtensions: function (target) {
                console.log("called");
                Object.preventExtensions(target);
                return true;
            }
        });
        Object.preventExtensions(proxy); // called true
    }
    {
        /*13、setPrototypeOf()：拦截Object.setPrototypeOf方法*/
        let handler = {
            setPrototypeOf(target, proto) {
                throw new Error('Changing the prototype is forbidden');
            }
        };
        let proto = {};
        let target = function () {
        };
        let proxy = new Proxy(target, handler);
        // console.log(proxy.setPrototypeOf(proxy, proto));
    }
    {
        /**Proxy.revocable()：返回一个可取消的Proxy实例*/
        let target = {};
        let handler = {};

        let {proxy, revoke} = Proxy.revocable(target, handler);
        proxy.foo = 123;
        console.log(proxy.foo); // 123
        revoke();
        // console.log(proxy.foo); // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
    }
    {
        /**this问题*/
        /*虽然 Proxy 可以代理针对目标对象的访问，但它不是目标对象的透明代理，即不做任何拦截的情况下，也无法保证与目标对象的行为一致。
        主要原因就是在 Proxy 代理的情况下，目标对象内部的this关键字会指向 Proxy 代理*/
        const target = {
            m: function () {
                console.log(this === proxy);
            }
        };
        const handler = {};
        const proxy = new Proxy(target, handler);
        console.log(target.m()); // false
        console.log(proxy.m()); // true

        /*下面是一个例子，由于this指向的变化，导致 Proxy 无法代理目标对象*/
        const _name = new WeakMap();

        class Person {
            constructor(name) {
                _name.set(this, name);
            }

            get name() {
                return _name.get(this);
            }
        }

        const jane = new Person('Jane');
        console.log(jane.name); // Jane
        const p = new Proxy(jane, {});
        console.log(p.name); // undefined
        /*上面代码中，目标对象jane的name属性，实际保存在外部WeakMap对象_name上面，通过this键区分。
        由于通过proxy.name访问时，this指向proxy，导致无法取到值，所以返回undefined。*/

        /*此外，有些原生对象的内部属性，只有通过正确的this才能拿到，所以 Proxy 也无法代理这些原生对象的属性。*/
        const target1 = new Date();
        const handler1 = {};
        const proxy1 = new Proxy(target1, handler1);
        // console.log(proxy1.getDate()); // Uncaught TypeError: this is not a Date object.
        /*上面代码中，getDate方法只能在Date对象实例上面拿到，如果this不是Date对象实例就会报错。这时，this绑定原始对象，就可以解决这个问题。*/

        const target2 = new Date('2015-01-01');
        const handler2 = {
            get(target2, prop) {
                if (prop === 'getDate') {
                    return target2.getDate.bind(target2);
                }
                return Reflect.get(target2, prop);
            }
        };
        const proxy2 = new Proxy(target2, handler2);
        console.log(proxy2.getDate()); // 1
    }
</script>
</body>
</html>